﻿Imports System.Reflection

''' <summary>
''' プロパティ読み取りクラス
''' </summary>
''' <remarks></remarks>
Public Class PropertyReader

    ''' <summary>
    ''' プロパティを取得します
    ''' </summary>
    ''' <param name="t"></param>
    ''' <param name="propertyName"></param>
    ''' <returns></returns>
    ''' <remarks>見つからない場合は例外が発生します</remarks>
    Public Shared Function ReadPropertyInfo(ByVal t As Type, ByVal propertyName As String) As PropertyInfo
        Try
            Dim prop As PropertyInfo = t.GetProperty(propertyName)
            If prop Is Nothing Then
                Throw New InvalidOperationException(String.Format("{0} クラスに {1} プロパティはありません。", t.Name, propertyName))
            End If
            Return prop
        Catch ex As Exception
            Throw New InvalidOperationException("プロパティの取得に失敗しました。", ex)
        End Try
    End Function

#Region "親プロパティの読み込み"
    ''' <summary>
    ''' 親プロパティを取得します
    ''' </summary>
    ''' <param name="myT"></param>
    ''' <param name="parentT"></param>
    ''' <returns></returns>
    ''' <remarks>見つからない場合は例外が発生します</remarks>
    Public Shared Function ReadParentPropertyInfo(ByVal myT As Type, ByVal parentT As Type, Optional ByVal propertyName As String = Nothing) As PropertyInfo
        Try
            Dim em As IEnumerable(Of PropertyInfo) = (From el As PropertyInfo In myT.GetProperties Where IsParentProeprtyInfo(el, parentT, propertyName))
            Select Case em.Count
                Case 0
                    Throw New InvalidOperationException(String.Format("{0} クラスに {1} 型のプロパティはありません。", myT.Name, parentT.Name))
                Case 1
                    Return em.First
                Case Else
                    Throw New InvalidOperationException(String.Format("{0} クラスに {1} 型のプロパティが複数みつかりました。", myT.Name, parentT.Name))
            End Select
        Catch ex As Exception
            Throw New InvalidOperationException("プロパティの取得に失敗しました。", ex)
        End Try
    End Function

    ''' <summary>
    ''' 親プロパティを取得します
    ''' 見つからない場合はNothingが返されます
    ''' </summary>
    ''' <param name="myT"></param>
    ''' <param name="parentT"></param>
    ''' <param name="propertyName"></param>
    ''' <returns></returns>
    ''' <remarks>見つからない場合はNothingが返されます</remarks>
    Public Shared Function ReadParentPropertyInfoOrNothing(ByVal myT As Type, ByVal parentT As Type, Optional ByVal propertyName As String = Nothing) As PropertyInfo
        Dim em As IEnumerable(Of PropertyInfo) = (From el As PropertyInfo In myT.GetProperties Where IsParentProeprtyInfo(el, parentT, propertyName))
        If em.Count <> 1 Then
            Return Nothing
        End If

        Return em.First
    End Function

    ''' <summary>
    ''' 親プロパティかどうかを判定します
    ''' </summary>
    ''' <param name="el"></param>
    ''' <param name="parentT"></param>
    ''' <param name="propertyName"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Shared Function IsParentProeprtyInfo(ByVal el As PropertyInfo, ByVal parentT As Type, ByVal propertyName As String) As Boolean
        If String.IsNullOrEmpty(propertyName) = False AndAlso el.Name <> propertyName Then
            Return False
        End If

        '#70「基底クラスにある子プロパティの場合、ChildCollectionFactory.Initで親子関係を自動補填できない」
        '型が一致する、または、派生関係にある場合、親とみなす
        Dim pt = el.PropertyType
        If pt.Equals(parentT) OrElse parentT.IsSubclassOf(pt) Then
            Return True
        End If

        Return False
    End Function
#End Region

#Region "子プロパティの読み込み"
    ''' <summary>
    ''' 子プロパティを取得します
    ''' </summary>
    ''' <param name="myT"></param>
    ''' <param name="childT"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function ReadChildPropertyInfo(ByVal myT As Type, ByVal childT As Type, Optional ByVal childProperty As String = Nothing) As PropertyInfo
        Try
            Select Case (From el As PropertyInfo In myT.GetProperties Where PropertyReader.IsChildProeprtyInfo(el, childT, childProperty)).Count
                Case 0
                    Throw New InvalidOperationException(String.Format("{0} クラスに {1} 型を格納するリストプロパティはありません。", myT.Name, childT.Name))
                Case 1
                    Return (From el As PropertyInfo In myT.GetProperties Where PropertyReader.IsChildProeprtyInfo(el, childT, childProperty)).First
                Case Else
                    Throw New InvalidOperationException(String.Format("{0} クラスに {1} 型を格納するリストプロパティが複数みつかりました。", myT.Name, childT.Name))
            End Select
        Catch ex As Exception
            Throw New InvalidOperationException("リストプロパティの取得に失敗しました。", ex)
        End Try
    End Function

    Private Shared Function IsChildProeprtyInfo(ByVal el As PropertyInfo, ByVal childT As Type, ByVal childProperty As String) As Boolean
        If String.IsNullOrEmpty(childProperty) = False AndAlso el.Name <> childProperty Then Return False
        '子でないものは除外
        If PropertyReader.IsChild(el) = False Then Return False

        'ジェネリックの型が指定されたものと合わない場合は除外
        If el.PropertyType.GetGenericArguments.First.FullName <> childT.FullName Then
            Return False
        Else
            Return True
        End If
    End Function
#End Region

#Region "プロパティが列、親、子であるかを判定します"
    Private Shared Cache As New Dictionary(Of PropertyInfo, Boolean)

    ''' <summary>
    ''' 列プロパティかどうかを判定します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend Shared Function IsColumn(ByVal prop As PropertyInfo) As Boolean
        If Cache.ContainsKey(prop) Then Return Cache(prop)

        Cache.Add(prop, IsColumnCore(prop))

        Return Cache(prop)
    End Function

    Private Shared Function IsColumnCore(ByVal prop As PropertyInfo) As Boolean
        'マッピングが無効の場合は除外
        If AttributeReader.Read(Of IgnoreAttribute)(prop).FirstOrDefault IsNot Nothing Then Return False

        'プロパティの型がSystemString型、もしくは、プロパティの型がIEnumerableインターフェイスを実装していない
        '配列（Byte()）は列とみなします
        If prop.PropertyType.FullName = GetType(String).FullName OrElse prop.PropertyType.FullName = "System.Byte[]" OrElse prop.PropertyType.GetInterface(GetType(IEnumerable).FullName) Is Nothing Then
            Return True
        Else
            Return False
        End If
    End Function

    ''' <summary>
    ''' 親プロパティかどうかを判定します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' 以下の条件を満たすものを、親プロパティとします
    ''' ・読み込み、書き込みができる
    ''' ・プロパティの型の名前空間がSystemで始まっていない
    ''' ・プロパティの型がジェネリック型ではない
    ''' ・マッピング無効属性が付いていない
    ''' なお、リレーション属性が取得できるかどうかまではチェックしていません
    ''' この点は、将来チェックに含めるかもしれません
    ''' </remarks>
    Public Shared Function IsParent(ByVal prop As PropertyInfo) As Boolean
        '列のうち、独自型のものを親とみなします
        '＃59　ただし、列挙体は親ではありません
        If IsColumn(prop) AndAlso prop.PropertyType.FullName.StartsWith("System.") = False AndAlso prop.PropertyType.IsEnum = False Then
            Return True
        Else
            Return False
        End If
    End Function

    ''' <summary>
    ''' 拡張コレクションかどうかを判定します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' #68
    ''' ・子であること
    ''' ・相手クラスの自クラスであり、プロパティ名が「Base」であること
    ''' </remarks>
    Public Shared Function IsExtensionCollection(prop As PropertyInfo) As Boolean
        If IsChild(prop) = False Then Return False

        Dim myType = prop.DeclaringType
        Dim yourType = prop.PropertyType.GetGenericArguments.First
        Dim q = (From el In yourType.GetProperties Where el.Name = "Base" AndAlso el.PropertyType.Equals(myType))
        If q.Any = False Then Return False

        Return True
    End Function

    ''' <summary>
    ''' 子プロパティかどうかを判定します
    ''' </summary>
    ''' <param name="prop"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' 以下の条件を満たすものを、子プロパティとします
    ''' ・プロパティの型がIListインターフェイスを実装している
    ''' ・プロパティの型がジェネリック型であり、かつ、ジェネリック型の引数は1である
    ''' ・マッピング無効属性が付いていない
    ''' なお、リレーション属性が取得できるかどうかまではチェックしていません
    ''' この点は、将来チェックに含めるかもしれません
    ''' </remarks>
    Public Shared Function IsChild(ByVal prop As PropertyInfo) As Boolean
        'マッピングが無効の場合は除外
        If AttributeReader.Read(Of IgnoreAttribute)(prop).FirstOrDefault IsNot Nothing Then Return False

        '読み書き可能でない場合は除外
        If prop.CanRead = False OrElse prop.CanWrite = False Then Return False

        'IEnumerableと互換性を持たない場合は除外
        If prop.PropertyType.GetInterface(GetType(IEnumerable).FullName) Is Nothing Then Return False

        'ジェネリック型であり、ジェネリックの引数が独自型である場合、子とみなす
        If prop.PropertyType.IsGenericType AndAlso prop.PropertyType.GetGenericArguments.Count = 1 AndAlso prop.PropertyType.GetGenericArguments.First.FullName.StartsWith("System.") = False Then
            Return True
        Else
            Return False
        End If
    End Function

#End Region

    ''' <summary>
    ''' 指定されたプロパティの値をリスト化します
    ''' </summary>
    ''' <typeparam name="MyT"></typeparam>
    ''' <typeparam name="ParentT"></typeparam>
    ''' <param name="instances"></param>
    ''' <param name="propertyName"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' Nothing、重複している場合はリストから除外されます
    ''' </remarks>
    Public Shared Function CreateValueList(Of MyT, ParentT)(ByVal instances As IEnumerable(Of MyT), Optional ByVal propertyName As String = Nothing) As List(Of ParentT)
        Dim prop As PropertyInfo = PropertyReader.ReadParentPropertyInfo(GetType(MyT), GetType(ParentT), propertyName)
        Dim lst As New List(Of ParentT)

        For Each item As MyT In instances
            Dim val As ParentT = prop.GetValue(item, Nothing)
            'Nothing、重複している場合は除外します
            If val Is Nothing OrElse lst.Contains(val) Then Continue For
            lst.Add(prop.GetValue(item, Nothing))
        Next

        Return lst
    End Function

    ''' <summary>
    ''' プロパティの型を取得します
    ''' Nullableの場合、格納できる型が返されます
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function GetActualPropertyType(prop As PropertyInfo) As Type
        Dim t As Type = Nothing

        If prop.PropertyType.IsGenericType Then
            'Nullableの場合、格納できる型をセット
            t = prop.PropertyType.GetGenericArguments(0)
        Else
            t = prop.PropertyType
        End If

        Return t
    End Function

    ''' <summary>
    ''' Nullable型かどうかを判定します
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function IsNullable(prop As PropertyInfo) As Boolean
        If AttributeReader.Read(Of NullableAttribute)(prop).Any Then Return True
        If prop.PropertyType.IsGenericType AndAlso prop.PropertyType.GetGenericTypeDefinition Is GetType(Nullable(Of )) Then Return True

        Return False
    End Function

End Class
